
/*
 * Copyright (C) 2008-2009 Archie L. Cobbs. All rights reserved.
 *
 * $Id: DatabaseUpdater.java 377 2010-03-11 20:50:18Z archie.cobbs $
 */

package org.dellroad.sidekar.schema;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Set;

import org.apache.log4j.Logger;
import org.dellroad.sidekar.Database;
import org.dellroad.sidekar.Item;
import org.dellroad.sidekar.Schema;
import org.dellroad.sidekar.schema.SchemaMismatchException;
import org.dellroad.sidekar.schema.SchemaModificationException;
import org.dellroad.sidekar.schema.model.SchemaModel;
import org.dellroad.stuff.graph.TopologicalSorter;
import org.dellroad.stuff.schema.AbstractSchemaUpdate;
import org.dellroad.stuff.schema.SchemaUpdate;

/**
 * Database updater. This class does the work of actually updating a database.
 *
 * <p>
 * It's most important functions are to properly order the {@link DatabaseUpdate}s so that the
 * predecessor constraints are satisfied, and to ensure that each update is only applied once.
 */
public class DatabaseUpdater {

    private final Logger log = Logger.getLogger(getClass());
    private final Database db;
    private final Schema sidekarSchema;
    private final LinkedHashMap<String, DatabaseUpdate> updateMap;  // mapping from update name to update
    private final List<? extends DatabaseUpdate> orderedUpdates;    // updates in the order they should be applied

    /**
     * Constructor.
     *
     * @param db sidekar database to be updated
     * @param sidekarSchema schema access object
     * @param updateList updates to apply, in the order in which they should be applied,
     *  predecessor constraints notwithstanding
     */
    public DatabaseUpdater(Database db, Schema sidekarSchema, List<? extends DatabaseUpdate> updateList) {
        this.db = db;
        this.sidekarSchema = sidekarSchema;
        this.updateMap = buildUpdateMap(updateList);
        this.orderedUpdates = buildOrderedUpdateList();
    }

    /**
     * Determine which updates need to be applied and apply them.
     *
     * @param schema the schema before applying any updates
     * @param updatesBefore contains the names of all updates that have already been applied
     * @param updatesApplied should be initially empty; the names of all updates that were
     *  actually applied will be added by this method
     */
    public void updateDatabase(SchemaModel schema, Set<String> updatesBefore, Set<String> updatesApplied) {

        // Verify no unknown updates have been applied
        for (String updateName : updatesBefore) {
            if (!this.updateMap.containsKey(updateName)) {
                throw new SchemaMismatchException(schema, "an unknown database update \"" + updateName
                  + "\" has been applied to database \"" + this.db.getName() + "\" (perhaps you downgraded the software?)");
            }
        }

        // Eliminate updates already applied and see if anything is left
        List<DatabaseUpdate> list = getUnappliedUpdates(updatesBefore);
        if (list.isEmpty()) {
            this.log.info("no database updates are required for \"" + this.db.getName() + "\"");
            return;
        }

        // Apply and record each update not already applied
        this.log.info("applying the following database updates to \"" + this.db.getName() + "\": " + list);
        for (DatabaseUpdate update : list) {

            // Do special secret configuration for structural schema updates
            if (update instanceof AbstractSchemaUpdate) {
                ((AbstractSchemaUpdate)update).configure(this.sidekarSchema, schema,
                  schema instanceof Item ? ((Item)schema).getSidekarEntity().getDatabase() : Instantiator.DEFAULT);
            } else if (update instanceof SchemaUpdate)
                throw new SchemaModificationException("user schema updates like \"" + update.getName() + "\" are not supported");

            // Apply update
            this.log.info("applying database update \"" + update.getName() + "\"");
            try {
                update.updateDatabase(this.db);
            } catch (SchemaModificationException e) {
                throw new SchemaModificationException("database update \"" + update.getName() + "\" failed: " + e.getMessage(), e);
            }

            // Add update name to applied list
            updatesApplied.add(update.getName());
        }
    }

    /**
     * Filter the already-applied updates out of the list and return the result.
     */
    private List<DatabaseUpdate> getUnappliedUpdates(Set<String> updatesBefore) {
        ArrayList<DatabaseUpdate> list = new ArrayList<DatabaseUpdate>();
        for (DatabaseUpdate update : this.orderedUpdates) {
            if (!updatesBefore.contains(update.getName()))
                list.add(update);
        }
        return list;
    }

    /**
     * Build mapping from update name to update.
     */
    private LinkedHashMap<String, DatabaseUpdate> buildUpdateMap(List<? extends DatabaseUpdate> updateList) {
        LinkedHashMap<String, DatabaseUpdate> map = new LinkedHashMap<String, DatabaseUpdate>(updateList.size());
        for (DatabaseUpdate update : updateList) {
            if (map.put(update.getName(), update) != null)
                throw new IllegalArgumentException("duplicate update \"" + update.getName() + "\" in database update list");
        }
        return map;
    }

    /**
     * Build a list of the updates in the order they should be applied. This ordering
     * is determined primarily by the predecessor constraints and secondarily by the
     * original ordering in the update list provided to the construtor. In other words,
     * it is a topological sort of the predecessor graph, with any ties broken by the
     * original ordering.
     *
     * <p>
     * This method does not check which updates have already been applied.
     */
    private List<DatabaseUpdate> buildOrderedUpdateList() {

        // Create edge lister defining update -> predecessor edges
        TopologicalSorter.EdgeLister<DatabaseUpdate> edgeLister = new TopologicalSorter.EdgeLister<DatabaseUpdate>() {
            public Set<DatabaseUpdate> getOutEdges(DatabaseUpdate update) {
                Set<String> targetNames = update.getRequiredPredecessors();
                HashSet<DatabaseUpdate> targets = new HashSet<DatabaseUpdate>(targetNames.size());
                for (String targetName : targetNames)
                    targets.add(DatabaseUpdater.this.updateMap.get(targetName));
                return targets;
            }
        };

        // Perform topological sort using reversed edges
        return new TopologicalSorter<DatabaseUpdate>(this.updateMap.values(), edgeLister).sortEdgesReversed();
    }
}

